/*
 * Arranges nodes based on relationships. Contains a series or rows (DiagramRow), 
 * each row contains a series of groups (DiagramGroup), and each group contains a 
 * series of nodes (DiagramNode).
 * 
 * Contains a list of connections. Each connection describes where the node
 * is located in the diagram and type of connection. The lines are draw
 * during OnRender.
 * 
 * Diagram is responsible for managing the rows. The logic that populates the rows
 * and understand all of the relationships is contained in DiagramLogic.
 *
*/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Input;
using System.Windows.Threading;
using FamilyShowSource.Events;
using FamilyShowSource.Models;

namespace FamilyShowSource.Views.Diagram
{
    /// <summary>
    /// Diagram that lays out and displays the nodes.
    /// </summary>
    public class Diagram : FrameworkElement
    {
        #region Constants & Fields

        /// <summary>
        /// Specified constants for this class
        /// </summary>
        private static class Const
        {
            /// <summary>
            /// Duration to pause before displaying new nodes.
            /// </summary>
            public const double AnimationPauseDuration = 600;

            /// <summary>
            /// Duration for nodes to fade in when the diagram is repopulated.
            /// </summary>
            public const double NodeFadeInDuration = 500;

            /// <summary>
            /// Duration for the new person animation.
            /// </summary>
            public const double NewPersonAnimationDuration = 250;

            /// <summary>
            /// Stop adding new rows when the number of nodes exceeds the max node limit.
            /// </summary>
            public const int MaximumNodes = 50;

            /// <summary>
            /// Group space.
            /// </summary>
            public const double PrimaryRowGroupSpace = 20;
            public const double ChildRowGroupSpace = 20;
            public const double ParentRowGroupSpace = 40;

            /// <summary>
            /// Amount of space between each row.
            /// </summary>
            public const double RowSpace = 40;

            /// <summary>
            /// Scale multiplier for spouse and siblings.
            /// </summary>
            public const double RelatedMultiplier = 0.8;

            /// <summary>
            /// Scale multiplier for each future generation row.
            /// </summary>
            public const double GenerationMultiplier = 0.9;
        }

        /// <summary>
        /// List of rows in the diagram. Each row contains groups, and each group contains nodes.
        /// </summary>
        private readonly List<DiagramRow> _rows = new List<DiagramRow>();

        /// <summary>
        /// Populates the rows with nodes.
        /// </summary>
        private readonly DiagramLogic _logic;

        /// <summary>
        /// Size of the diagram. Used to layout all of the nodes before the control gets an actual size.
        /// </summary>
        private Size _totalSize = new Size(0, 0);

        /// <summary>
        /// Zoom level of the diagram.
        /// </summary>
        private double _scale = 1.0;

        /// <summary>
        /// Bounding area of the selected node, the selected node is the 
        /// non-primary node that is selected, and will become the primary node.
        /// </summary>
        private Rect _selectedNodeBounds = Rect.Empty;

        /// <summary>
        /// Flag if currently populating or not. Necessary since diagram populate 
        /// contains several parts and animations, request to update the diagram
        /// are ignored when this flag is set.
        /// </summary>
        private bool _populating;

        /// <summary>
        /// The person that has been added to the diagram.
        /// </summary>
        private Person _newPerson;

        /// <summary>
        /// Timer used with the repopulating animation.
        /// </summary>
        private readonly DispatcherTimer _animationTimer = new DispatcherTimer();

#if DEBUG
        /// <summary>
        /// Flag if the row and group borders should be drawn.
        /// </summary>
        private bool _displayBorder;
#endif

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the zoom level of the diagram.
        /// </summary>
        public double Scale
        {
            get { return this._scale; }
            set
            {
                if (this._scale != value)
                {
                    _scale = value;
                    this.LayoutTransform = new ScaleTransform(_scale, _scale);
                }
            }
        }

        /// <summary>
        /// Sets the display year filter.
        /// </summary>
        public double DisplayYear
        {
            set
            {
                // Filter nodes and connections based on the year.
                _logic.DisplayYear = value;
                this.InvalidateVisual();
            }
        }

        /// <summary>
        /// Gets the minimum year specified in the nodes and connections.
        /// </summary>
        public double MinimumYear
        {
            get { return _logic.MinimumYear; }
        }

        /// <summary>
        /// Gets the bounding area (relative to the diagram) of the primary node.
        /// </summary>
        public Rect PrimaryNodeBounds
        {
            get { return _logic.GetNodeBounds(_logic.Family.Current); }
        }

        /// <summary>
        /// Gets the bounding area (relative to the diagram) of the selected node.
        /// The selected node is the non-primary node that was previously selected
        /// to be the primary node.
        /// </summary>
        public Rect SelectedNodeBounds
        {
            get { return _selectedNodeBounds; }
        }

        /// <summary>
        /// Gets the number of nodes in the diagram.
        /// </summary>
        public int NodeCount
        {
            get { return _logic.PersonLookup.Count; }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// The default constructor
        /// </summary>
        public Diagram()
        {
            // Init the diagram logic, which handles all of the layout logic.
            this._logic = new DiagramLogic();
            this.Loaded += DiagramLoaded;
        }

        #endregion

        #region Methods

        #region Publishes & Subscribes

        /// <summary>
        /// Publish event to nofify that the diagram has been populated
        /// </summary>
        private void NotifyDiagramPopulated()
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyDiagramPopulatedEvent>().Publish(null);
        }

        #endregion

        #region Layout Diagram

        /// <summary>
        /// Return the number of rows.
        /// </summary>
        protected override int VisualChildrenCount
        {
            get { return _rows.Count; }
        }

        /// <summary>
        /// Return the requested row.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        protected override Visual GetVisualChild(int index)
        {
            return _rows[index];
        }

        /// <summary>
        /// Measure size of the diagram
        /// The size of the diagram will be computed base on the size of child rows
        /// </summary>
        /// <param name="availableSize"></param>
        /// <returns></returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            // Let each row determine how large they want to be.
            var size = new Size(double.PositiveInfinity, double.PositiveInfinity);
            foreach (DiagramRow row in _rows)
                row.Measure(size);

            // Return the total size of the diagram.
            return ArrangeRows(false);
        }

        /// <summary>
        /// Arrange the rows in the diagram, return the total size.
        /// </summary>
        /// <param name="finalSize"></param>
        /// <returns></returns>
        protected override Size ArrangeOverride(Size finalSize)
        {
            return ArrangeRows(true);
        }

        /// <summary>
        /// Arrange the rows in the diagram, return the total size.
        /// </summary>
        private Size ArrangeRows(bool arrange)
        {
            // Location of the row.
            double pos = 0;

            // Bounding area of the row.
            var bounds = new Rect();

            // Total size of the diagram.
            var size = new Size(0, 0);

            foreach (DiagramRow row in _rows)
            {
                // Row location, center the row horizontaly.
                bounds.Y = pos;
                bounds.X = (_totalSize.Width == 0) ? 0 :
                    bounds.X = (_totalSize.Width - row.DesiredSize.Width) / 2;

                // Row Size.
                bounds.Width = row.DesiredSize.Width;
                bounds.Height = row.DesiredSize.Height;

                // Arrange the row, save the location.
                if (arrange)
                {
                    row.Arrange(bounds);
                    row.Location = bounds.TopLeft;
                }
                
                // Update the size of the diagram.
                size.Width = Math.Max(size.Width, bounds.Width);
                size.Height = pos + row.DesiredSize.Height;

                pos += bounds.Height;
            }

            // Store the size, this is necessary so the diagram
            // can be laid out without a valid Width property.
            _totalSize = size;
            return size;
        }

        /// <summary>
        /// Draw the connector lines at a lower level (OnRender) instead
        /// of creating visual tree objects.
        /// </summary>
        protected override void OnRender(DrawingContext drawingContext)
        {
#if DEBUG
            if (_displayBorder)
            {
                // Draws borders around the rows and groups.
                foreach (DiagramRow row in _rows)
                {
                    // Display row border.
                    var bounds = new Rect(row.Location, row.DesiredSize);
                    drawingContext.DrawRectangle(null, new Pen(Brushes.DarkKhaki, 1), bounds);

                    foreach (DiagramGroup group in row.Groups)
                    {
                        // Display group border.
                        bounds = new Rect(group.Location, group.DesiredSize);
                        bounds.Offset(row.Location.X, row.Location.Y);
                        bounds.Inflate(-1, -1);
                        drawingContext.DrawRectangle(null, new Pen(Brushes.Gray, 1), bounds);
                    }
                }
            }
#endif

            // Draw child connectors first, so marriage information appears on top.
            //foreach (DiagramConnector connector in _logic.Connections)
            //{
            //    if (connector.IsChildConnector)
            //        connector.Draw(drawingContext);
            //}

            //// Draw all other non-child connectors.
            //foreach (DiagramConnector connector in _logic.Connections)
            //{
            //    if (!connector.IsChildConnector)
            //        connector.Draw(drawingContext);
            //}
        }

        #endregion

        #region Diagram Updates

        /// <summary>
        /// This method is invoked when the diagram has been loaded
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DiagramLoaded(object sender, RoutedEventArgs e)
        {
            this.FamilyCurrentChanged();
        }

        /// <summary>
        /// Called when the current person in the main People collection changes.
        /// This means the diagram should be updated based on the new selected person.
        /// </summary>
        public void FamilyCurrentChanged()
        {
            // Save the bounds for the current primary person, this 
            // is required later when animating the diagram.
            this._selectedNodeBounds = this._logic.GetNodeBounds(this._logic.Family.Current);

            // Repopulate the diagram.
            this.Populate();
        }

        /// <summary>
        /// Reset all of the data associated with the diagram.
        /// </summary>
        private void Clear()
        {
            foreach (DiagramRow row in this._rows)
            {
                row.Clear();
                this.RemoveVisualChild(row);
            }

            this._rows.Clear();
            this._logic.Clear();
        }

        /// <summary>
        /// Populate the diagram. Update the diagram and hide all non-primary nodes.
        /// Then pause, and finish the populate by fading in the new nodes.
        /// </summary>
        private void Populate()
        {
            // Set flag to ignore future updates until complete.
            this._populating = true;

            // Update the nodes in the diagram.
            this.UpdateDiagram();

            // First hide all of the nodes except the primary node.
            //foreach (DiagramConnectorNode connector in _logic.PersonLookup.Values)
            //{
            //    if (connector.Node.Person != _logic.Family.Current)
            //        connector.Node.Visibility = Visibility.Hidden;
            //}

            // Required to update (hide) the connector lines.            
            this.InvalidateVisual();
            this.InvalidateArrange();
            this.InvalidateMeasure();

            // Pause before displaying the new nodes.
            this._animationTimer.Interval = App.GetAnimationDuration(Const.AnimationPauseDuration);
            this._animationTimer.Tick += new EventHandler(OnAnimationTimer);
            this._animationTimer.IsEnabled = true;

            // Let other controls know the diagram has been repopulated.
            this.NotifyDiagramPopulated();
        }

        /// <summary>
        /// The animation pause timer is complete, finish populating the diagram.
        /// </summary>
        private void OnAnimationTimer(object sender, EventArgs e)
        {
            // Turn off the timer.
            _animationTimer.IsEnabled = false;

            // Fade each node into view.
            //foreach (DiagramConnectorNode connector in _logic.PersonLookup.Values)
            //{
            //    if (connector.Node.Visibility != Visibility.Visible)
            //    {
            //        connector.Node.Visibility = Visibility.Visible;
            //        connector.Node.BeginAnimation(Diagram.OpacityProperty,
            //            new DoubleAnimation(0, 1,
            //            App.GetAnimationDuration(Const.NodeFadeInDuration)));
            //    }
            //}

            // Redraw connector lines.
            this.InvalidateVisual();

            _populating = false;
        }

        /// <summary>
        /// Reset the diagram with the nodes. This is accomplished by creating a series of rows.
        /// Each row contains a series of groups, and each group contains the nodes. The elements 
        /// are not laid out at this time. Also creates the connections between the nodes.
        /// </summary>
        private void UpdateDiagram()
        {
            // Necessary for Blend.
            if (this._logic.Family == null)
                return;

            // First reset everything.
            this.Clear();

            // Nothing to draw if there is not a primary person.
            if (this._logic.Family.Current == null)
                return;

            // Primary row.
            var primaryPerson = _logic.Family.Current;
            var primaryRow = _logic.CreatePrimaryRow(primaryPerson, 1.0, Const.RelatedMultiplier);
            primaryRow.GroupSpace = Const.PrimaryRowGroupSpace;
            this.AddRow(primaryRow);

            // Create as many rows as possible until exceed the max node limit.
            // Switch between child and parent rows to prevent only creating
            // child or parents rows (want to create as many of each as possible).
            var nodeCount = this.NodeCount;

            // The scale values of future generations, this makes the nodes
            // in each row slightly smaller.
            var nodeScale = 1.0;

            var childRow = primaryRow;
            var parentRow = primaryRow;

            while (nodeCount < Const.MaximumNodes && (childRow != null || parentRow != null))
            {
                // Child Row.
                if (childRow != null)
                    childRow = AddChildRow(childRow);

                // Parent row.
                if (parentRow != null)
                {
                    nodeScale *= Const.GenerationMultiplier;
                    parentRow = AddParentRow(parentRow, nodeScale);
                }

                // See if reached node limit yet.                                       
                nodeCount = this.NodeCount;
            }

            // Raise event so others know the diagram was updated.
            //OnDiagramUpdated();

            // Animate the new person (optional, might not be any new people).
            //AnimateNewPerson();
        }

        /// <summary>
        /// Add a child row to the diagram.
        /// </summary>
        private DiagramRow AddChildRow(DiagramRow row)
        {
            // Get list of children for the current row.
            //var children = DiagramLogic.GetChildren(row);
            //if (children.Count == 0)
            //    return null;

            //// Add bottom space to existing row.
            //row.Margin = new Thickness(0, 0, 0, Const.RowSpace);

            //// Add another row.
            //var childRow = _logic.CreateChildrenRow(children, 1.0, Const.RelatedMultiplier);
            //childRow.GroupSpace = Const.ChildRowGroupSpace;
            //AddRow(childRow);

            return null;// childRow;
        }

        /// <summary>
        /// Add a parent row to the diagram.
        /// </summary>
        private DiagramRow AddParentRow(DiagramRow row, double nodeScale)
        {
            // Get list of parents for the current row.
            //var parents = DiagramLogic.GetParents(row);
            //if (parents.Count == 0)
            //    return null;

            //// Add another row.
            //var parentRow = _logic.CreateParentRow(parents, nodeScale, nodeScale * Const.RelatedMultiplier);
            //parentRow.Margin = new Thickness(0, 0, 0, Const.RowSpace);
            //parentRow.GroupSpace = Const.ParentRowGroupSpace;
            //InsertRow(parentRow);

            return null;// parentRow;
        }

        /// <summary>
        /// Add a row to the visual tree.
        /// </summary>
        private void AddRow(DiagramRow row)
        {
            if (row != null && row.NodeCount > 0)
            {
                this.AddVisualChild(row);
                _rows.Add(row);
            }
        }

        /// <summary>
        /// Insert a row in the visual tree.
        /// </summary>
        private void InsertRow(DiagramRow row)
        {
            if (row != null && row.NodeCount > 0)
            {
                this.AddVisualChild(row);
                _rows.Insert(0, row);
            }
        }

#if DEBUG
        void OnToggleBorderClick(object sender, RoutedEventArgs e)
        {
            // Display or hide the row and group borders.
            _displayBorder = !_displayBorder;
            
            // Update check on menu.
            var menuItem = this.ContextMenu.Items[0] as MenuItem;
            menuItem.IsChecked = _displayBorder;
            
            this.InvalidateVisual();
        }
#endif

        /// <summary>
        /// A node was clicked, make that node the primary node. 
        /// </summary>
        private void OnNodeClick(object sender, RoutedEventArgs e)
        {
            // Get the node that was clicked.
            var node = sender as DiagramNode;
            if (node != null)
            {
                // Make it the primary node. This raises the CurrentChanged
                // event, which repopulates the diagram.
                _logic.Family.Current = node.Person;
            }
        }

        /// <summary>
        /// Animate the new person that was added to the diagram.
        /// </summary>
        private void AnimateNewPerson()
        {
            // The new person is optional, can be null.
            if (_newPerson == null)
                return;

            // Get the UI element to animate.                
            var node = _logic.GetDiagramNode(_newPerson);
            if (node != null)
            {
                // Create the new person animation.
                var anim = new DoubleAnimation(0, 1,
                    App.GetAnimationDuration(Const.NewPersonAnimationDuration));

                // Animate the node.
                var transform = new ScaleTransform();
                transform.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
                transform.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
                node.RenderTransform = transform;
            }

            _newPerson = null;
        }

        #endregion

        #endregion
    }
}
